iOS中的圆角处理(终结篇)

前言

在开发中,我们经常用到圆角的处理,圆角看起来会比直角更加美观和柔美。但是设置圆角往往会带来一定的性能损耗,损耗的来源主要由于大量的离屏渲染,接下来我们就来讲一下如果实现高性能的圆角。我们下面来看以下几种设置圆角的方式以及它们对性能的影响。

CornerRadius+masksToBounds

第一种方式就是我们最常用的设置圆角的方式,我们首先来看看cornerRadius的定义是什么:

The radius to use when drawing rounded corners for the layer’s background. Animatable.
Setting the radius to a value greater than 0.0 causes the layer to begin drawing rounded corners on its background. By default, the corner radius does not apply to the image in the layer’s contents property; it applies only to the background color and border of the layer. However, setting the masksToBounds property to YES causes the content to be clipped to the rounded corners.
The default value of this property is 0.0.

通过上面的解释我们可以看到cornerRadius只是对view的背景颜色和边框起作用,对于一些像ImageView以及Label等含有内部子视图的就不起作用了,这个时候我们往往还要添下下面这一句来让label生成一个适配圆角的剪切蒙版,该蒙版与label的边界相匹配,这样就达到了圆角的效果。

1
label.layer.masksToBounds = YES;

但是这样就会导致另外一个问题,离屏渲染。前面我们已经说了大量的离屏渲染会导致性能下降,最直观的感受就是如果在一个tableView中有大量的离屏渲染,就会导致FPS下降导致掉帧,界面看起来卡顿。

性能影响

我们通过模拟器的 Color-Off-screen-Render可以看到那个元素产生了离屏渲染了,和很多人说的不一样,不可以统筹的说创建圆角是产生离屏渲染的原因
正确的来说:

1
2
view.layer.cornerRadius = 5;
view.layer.maskToBounds = YES;

这两句代码合在一起才是产生离屏渲染的原因。如下图所示,每个cell的第一和第二个的ImageView都是执行了上面的两句代码,产生了离屏幕渲染,而第三个棕色的圆形则是一个view,代码里面仅使用了view.layer.cornerRadius,所以没有产生离屏渲染。

si

如上所示,当一个tableView的离屏渲染达到了44个的时候,FPS下降到了35左右,性能下降得很厉害。所以如果一个界面上需要有很多的圆角的时候,这种方式不可取。

CAShapeLayer+UIBezierPath

这种方式简直是噩梦,我们先来看代码:

1
2
3
 CAShapeLayer *mask = [CAShapeLayer new];
mask.path = [UIBezierPath bezierPathWithRoundedRect:imageView.bounds cornerRadius:10].CGPath;
imageView.layer.mask = mask;

我们首先创建一个mask,然后通过贝塞尔曲线覆盖到原来的imageView上面,几行代码搞定。

性能影响

然后我们来看看界面是否会产生离屏渲染。

si

如果不看一下真的吓一跳,全部的元素都产生了离屏渲染,还不如第一种方式。然后我们看一下FPS是10,其实都不用看FPS,直接页面上都能看出来了,卡到怀疑人生。

上面的方式是创建子控件的时候直接添加mask,还有一种方式是在 -(void)drawRect:(CGRect)rect里面添加mask,这样方式更加糟糕,因为不恰当的使用这个方法会导致内存暴增。举个例子,iPhone6 上与屏幕等大的 UIView,即使重写一个空的 drawRect 方法,它也至少占用 750 1134 4 字节 ≈ 3.4 Mb 的内存。在 内存恶鬼drawRect 及其后续中,作者详细介绍了其中原理,据他测试,在 iPhone6 上空的、与屏幕等大的视图重写 drawRect 方法会消耗 5.2 Mb 内存。总之,能避免重写 drawRect 方法就尽可能避免。

Core Graphics

我们知道UIView其实是由CALayer和UIResponder组成,一个负责显示,一个负责响应。但是CALayer也只是一个普通的类,它并不能直接渲染到屏幕上,我们看到屏幕上面的东西其实也只是一张张图片,那为什么我们可以看到CALayer的内容呢。因为CALayer有一个content的属性,该属性可以传一个id类型的对象,当你传的对象为CGImage的时候,才会显示出来。

那通过Core Graphics 我们可以画出一个具有圆角的图片,然后添加到layer上面。

不过UIVIew和UIImageView的实现方式不一样,UIView的方式是创建一个空白的图片,然后插入到视图的最下面。而UIImageView是将在原来Image的基础上重新绘制一张带有圆角的图片,然后赋值给ImageView。

以下是关键代码:

UIVIew:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
- (UIImage *)lm_drawRectWithRoundedCorner:(CGFloat)radius
borderWidth:(CGFloat)borderWidth
borderColor:(UIColor *)borderColor
backGroundColor:(UIColor*)bgColor{
CGSize size = self.bounds.size;
UIGraphicsBeginImageContextWithOptions(size, NO, [UIScreen mainScreen].scale);
CGContextRef contextRef = UIGraphicsGetCurrentContext();

CGContextSetLineWidth(contextRef, borderWidth);
CGContextSetStrokeColorWithColor(contextRef, borderColor.CGColor);
CGContextSetFillColorWithColor(contextRef, bgColor.CGColor);

CGFloat halfBorderWidth = borderWidth / 2.0;
CGFloat width = size.width;
CGFloat height = size.height;

CGContextMoveToPoint(contextRef, width - halfBorderWidth, radius + halfBorderWidth);
CGContextAddArcToPoint(contextRef, width - halfBorderWidth, height - halfBorderWidth, width - radius - halfBorderWidth, height - halfBorderWidth, radius); // 右下角角度
CGContextAddArcToPoint(contextRef, halfBorderWidth, height - halfBorderWidth, halfBorderWidth, height - radius - halfBorderWidth, radius); // 左下角角度
CGContextAddArcToPoint(contextRef, halfBorderWidth, halfBorderWidth, width - halfBorderWidth, halfBorderWidth, radius); // 左上角
CGContextAddArcToPoint(contextRef, width - halfBorderWidth, halfBorderWidth, width - halfBorderWidth, radius + halfBorderWidth, radius); // 右上角
CGContextDrawPath(contextRef, kCGPathFillStroke);
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}

UIImageVIew:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (UIImage *)lm_drawRectWithRoundedCorner:(CGFloat)radius
size:(CGSize)size{

CGRect rect = CGRectMake(0, 0, size.width, size.height);

UIGraphicsBeginImageContextWithOptions(rect.size, false, [UIScreen mainScreen].scale);
CGContextRef context = UIGraphicsGetCurrentContext();

UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:rect byRoundingCorners:UIRectCornerAllCorners cornerRadii:CGSizeMake(radius, radius)];
CGContextAddPath(context, path.CGPath);

CGContextClip(context);

[self drawInRect:rect];
CGContextDrawPath(context, kCGPathFillStroke);
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}

性能影响

si

从上面的图片我们可以看出,设置圆角已经没有离屏渲染了,通过Core Animation可以看出FPS也回到60左右,界面也没有卡顿了。

注意:即使我们使用了这种方式,我们还是要谨慎直接设置view.backGroundColor,因为我们没有设置maskToBounds属性,所以以这样的方式设置了背景颜色依然会导致没有圆角效果。
如果想要背景颜色,可以在画圆角图片的时候改变一下backGroundColor。

总结

通过上面的分析,我们得出以下的结论:

  1. layer.cornerRadius不会触发离屏渲染,该属性只是对边框和背景颜色起作用,适用于内部没有其他控件的view。
  2. CAShapeLayer+UIBezierPath会触发离屏渲染。
  3. 最好的方式就是使用Core Graphics的方式绘制圆角图片。
  4. 当然,还是那句话,根据场景来使用,如果界面中圆角的地方不多,第一种方式是最简单快捷,效率最高的。如果用到的圆角很多,那还是使用Core Graphics的方式把。

代码

本文章的代码已经放到Github,需要的可以自取。CornerViewDemo

-------评论系统采用disqus,如果看不到需要翻墙-------------